// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats;

import bats.map.Blocks;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetManager;
import com.jme3.asset.AssetNotFoundException;
import com.jme3.asset.TextureKey;
import com.jme3.audio.AudioNode;
import com.jme3.cursors.plugins.JmeCursor;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.terrain.heightmap.AbstractHeightMap;
import com.jme3.terrain.heightmap.ImageBasedHeightMap;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.Misc;
import jme3utilities.MyAsset;
import jme3utilities.MySpatial;
import jme3utilities.MyString;
import jme3utilities.WaterProcessor;
import jme3utilities.xml.XmlLoader;
import org.w3c.dom.Document;

/**
 * Utility methods based on the application's asset manager. The first method
 * invoked should be setManager().
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class Assets {
    // *************************************************************************
    // constants

    /**
     * color of an active indicator
     */
    final private static ColorRGBA activeColor = ColorRGBA.White;
    /**
     * color of an inactive indicator
     */
    final private static ColorRGBA inactiveColor = ColorRGBA.Blue;
    /**
     * transparency of simple water (in meters, &gt;0)
     */
    final private static float waterTransparency = 2f;
    /**
     * wave height for simple water (&ge;0)
     */
    final private static float waveHeight = 0.15f;
    /**
     * wave speed for simple water (&gt;0)
     */
    final private static float waveSpeed = 0.03f;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(Assets.class.getName());
    /**
     * asset path for Particle material definition
     */
    final private static String particleMaterialAssetPath =
            "Common/MatDefs/Misc/Particle.j3md";
    /**
     * asset path of the shaded material definition
     */
    final private static String shadedMaterialAssetPath =
            "Common/MatDefs/Light/Lighting.j3md";
    /**
     * asset path of the (unshaded) Terrain material definition
     */
    final private static String terrainMaterialAssetPath =
            "Common/MatDefs/Terrain/Terrain.j3md";
    // *************************************************************************
    // fields
    /**
     * which asset manager: set by initialize()
     */
    private static AssetManager manager = null;
    /**
     * flag to enable shaded materials
     */
    private static boolean shadingFlag = true;
    // *************************************************************************
    // constructors

    /**
     * A private constructor to inhibit instantiation of this class.
     */
    private Assets() {
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Create unshaded material for a pick indicator or the 3D cursor.
     *
     * @param isActive true if the indicator is active, false if inactive
     * @return a new instance
     */
    public static Material createIndicatorMaterial(boolean isActive) {
        ColorRGBA color = isActive ? activeColor : inactiveColor;
        Material material = createUnshadedMaterial(color);
        return material;
    }

    /**
     * Create a material from the specified diffuse texture.
     *
     * @param assetPath to diffuse texture (not null)
     * @return a new instance
     */
    public static Material createMaterial(String assetPath) {
        assert assetPath != null;

        Texture texture = loadTexture(assetPath, true);
        Material material = createMaterial(texture);
        return material;
    }

    /**
     * Create a particle material from the specified flipped texture.
     *
     * @param assetPath to the texture (not null)
     * @return a new instance
     */
    public static Material createParticleMaterial(String assetPath) {
        assert assetPath != null;

        Material material = loadMaterial(particleMaterialAssetPath);
        boolean flipY = true;
        Texture texture = loadTexture(assetPath, flipY);
        material.setTexture("Texture", texture);

        return material;
    }

    /**
     * Create a terrain material without tri-planar mapping.
     *
     * @param alphaName name of alpha map (not null)
     * @param textureScale (&gt;0)
     * @param tex1 name of block texture for Tex1 (not null)
     * @param tex2 name of block texture for Tex2 (not null)
     * @param tex3 name of block texture for Tex3 (not null)
     * @return a new instance
     */
    public static Material createTerrainMaterial(String alphaName,
            float textureScale, String tex1, String tex2, String tex3) {
        assert alphaName != null;
        assert textureScale > 0f : textureScale;
        assert tex1 != null;
        assert tex2 != null;
        assert tex3 != null;
        /*
         * Create a generic terrain material.
         */
        Material material = createTerrainMaterial();
        /*
         * Load the alpha texture which determines the splat texture for
         * each location.
         */
        String alphaAssetPath =
                String.format("Textures/terrain/alpha/%s.png", alphaName);
        Texture alphaTexture = loadTexture(alphaAssetPath);
        material.setTexture("Alpha", alphaTexture);
        /*
         * Load the splat textures.
         */
        Texture texture = loadBlockTexture(tex1);
        material.setTexture("Tex1", texture);
        material.setFloat("Tex1Scale", textureScale);

        texture = loadBlockTexture(tex2);
        material.setTexture("Tex2", texture);
        material.setFloat("Tex2Scale", textureScale);

        texture = loadBlockTexture(tex3);
        material.setTexture("Tex3", texture);
        material.setFloat("Tex3Scale", textureScale);

        return material;
    }

    /**
     * Create an unshaded material with the specified color.
     *
     * @param color which color (not null, unaffected)
     * @return a new instance
     */
    public static Material createUnshadedMaterial(ColorRGBA color) {
        assert color != null;

        Material material = MyAsset.createUnshadedMaterial(manager);
        material.setColor("Color", color);
        return material;
    }

    /**
     * Create a processor for simple water.
     *
     * @param scaleFactor (&gt;0)
     * @return a new instance
     */
    public static WaterProcessor createWater(float scaleFactor) {
        assert scaleFactor > 0f : scaleFactor;

        WaterProcessor result = new WaterProcessor(manager);
        /*
         * For the reflection camera, clip everything below the surface.
         */
        result.setReflectionClippingOffset(0f);
        /*
         * Set water and wave parameters.
         */
        result.setDistortionScale(waveHeight);
        result.setWaterDepth(waterTransparency / scaleFactor);
        result.setWaveSpeed(waveSpeed);

        return result;
    }

    /**
     * Create a wire frame material for debugging.
     *
     * @param color (not null)
     * @return a new instance
     */
    public static Material createWireframeMaterial(ColorRGBA color) {
        assert color != null;

        Material material = createUnshadedMaterial(color);
        material.getAdditionalRenderState().setWireframe(true);

        return material;
    }

    /**
     * Access the asset manager.
     *
     * @return the pre-existing instance (not null)
     */
    public static AssetManager getManager() {
        assert manager != null;
        return manager;
    }

    /**
     * Load a material for a block, prop, or similar object.
     *
     * @param materialName (not null)
     * @return a new instance
     */
    public static Material loadBlockMaterial(String materialName) {
        assert materialName != null;

        Texture texture = loadBlockTexture(materialName);
        Material material = createMaterial(texture);
        return material;
    }

    /**
     * Load the geometries of a block.
     *
     * @param blockFolder which kind of block (not null)
     * @return a new orphan node
     */
    public static Node loadBlockModel(String blockFolder) {
        assert blockFolder != null;

        String blockType = Blocks.getType(blockFolder);
        String assetPath = String.format("Models/blocks/%s/%s.j3o",
                blockFolder, blockType);
        Node result = loadModel(assetPath);

        return result;
    }

    /**
     * Load a buffered audio node from an asset.
     *
     * @param assetPath to the audio asset (not null)
     * @return a new instance
     */
    public static AudioNode loadBufferedAudio(String assetPath) {
        assert assetPath != null;

        AudioNode audioNode = new AudioNode(manager, assetPath, false);
        audioNode.setName(assetPath);
        return audioNode;
    }

    /**
     * Load a cursor from an asset.
     *
     * @param path from assets/Textures/indicators to the cursor asset (not
     * null)
     * @param spotX hotspot X-coordinate (pixels from left edge of image)
     * @param spotY hotspot Y-coordinate (pixels from bottom edge of image)
     * @return a new instance
     */
    public static JmeCursor loadCursor(String path, int spotX, int spotY) {
        String assetPath = "Textures/cursors/" + path;
        JmeCursor cursor = (JmeCursor) manager.loadAsset(assetPath);
        /*
         * Set the hot spot to work around issue #638.
         */
        cursor.setxHotSpot(spotX);
        cursor.setyHotSpot(spotY);
        return cursor;
    }

    /**
     * Load a height map.
     *
     * @param mapName name of the height-map asset (not null)
     * @return a new instance
     */
    public static AbstractHeightMap loadHeightMap(String mapName) {
        assert mapName != null;

        String assetPath =
                String.format("Textures/terrain/height/%s.jpg", mapName);
        Texture heightTexture = loadTexture(assetPath);
        Image heightImage = heightTexture.getImage();
        float heightScale = 1f;
        AbstractHeightMap heightMap =
                new ImageBasedHeightMap(heightImage, heightScale);
        heightMap.load();
        return heightMap;
    }

    /**
     * Load the geometry of an indicator.
     *
     * @param indicatorType which kind of indicator (not null)
     * @return a new, orphan geometry
     */
    public static Geometry loadIndicatorModel(String indicatorType) {
        assert indicatorType != null;

        String assetPath = String.format("Models/indicators/%s/%s.j3o",
                indicatorType, indicatorType);
        Geometry geometry = loadSimpleModel(assetPath);

        return geometry;
    }

    /**
     * Load a material.
     *
     * @param assetPath to the material (not null)
     * @return a new instance
     */
    public static Material loadMaterial(String assetPath) {
        assert assetPath != null;

        Material material = new Material(manager, assetPath);
        return material;
    }

    /**
     * Load a model and check the number of geometries.
     *
     * @param assetPath to the model (not null)
     * @param numGeometries number of geometries expected (&gt;0)
     * @return a new orphan node with numGeometries new child geometries
     */
    public static Node loadModel(String assetPath, int numGeometries) {
        assert assetPath != null;
        assert numGeometries > 0 : numGeometries;

        Node node = loadModel(assetPath);
        /*
         * Discard extra nodes -- nodes whose only child is another node.
         */
        while (node.getChildren().size() == 1) {
            Spatial child = node.getChild(0);
            if (!(child instanceof Node)) {
                break;
            }
            node = (Node) child;
        }
        /*
         * Check whether all children are geometries.
         */
        for (Spatial child : node.getChildren()) {
            if (!(child instanceof Geometry)) {
                throw new AssetLoadException(
                        "unexpected spatial loaded from " + assetPath);
            }
        }
        /*
         * Check the number of geometries.
         */
        int childCount = node.getChildren().size();
        if (childCount != numGeometries) {
            throw new AssetLoadException(
                    "wrong number of geometries loaded from " + assetPath);
        }

        return node;
    }

    /**
     * Load a non-flipped texture asset in repeat mode with nearest filters.
     *
     * @param assetPath to the texture (not null)
     * @return the texture which was loaded
     */
    public static Texture loadNearestTexture(String assetPath) {
        assert assetPath != null;

        Texture texture = Assets.loadTexture(assetPath);
        texture.setMinFilter(Texture.MinFilter.NearestNoMipMaps);
        texture.setMagFilter(Texture.MagFilter.Nearest);
        return texture;
    }

    /**
     * Pilums have a glossy black material in an attempt to match the otos.
     *
     * @return a new instance
     */
    public static Material loadPilumMaterial() {
        float shininess = 3f;
        Material material = createShadedMaterial(ColorRGBA.Black, shininess,
                ColorRGBA.White);
        return material;
    }

    /**
     * Load the geometries of a prop.
     *
     * @param propType which kind of prop (not null)
     * @return a new, orphan node (or null if model error)
     */
    public static Node loadPropModel(String propType) {
        assert propType != null;

        String assetPath = String.format("Models/props/%s/%s.j3o",
                propType, propType);
        Node result = loadModel(assetPath);

        int numObjects = result.getChildren().size();
        if (numObjects != 1) {
            logger.log(Level.SEVERE,
                    "Model for prop {0} is not a single object.",
                    MyString.quote(propType));
            assert false;
            return null;
        }
        return result;
    }

    /**
     * Load a model consisting of a single geometry.
     *
     * @param assetPath to the model (not null)
     * @return a new orphan geometry
     */
    public static Geometry loadSimpleModel(String assetPath) {
        assert assetPath != null;

        int numGeometries = 1;
        Node model = loadModel(assetPath, numGeometries);
        Geometry geometry = (Geometry) model.getChild(0);
        geometry.removeFromParent();

        return geometry;
    }

    /**
     * Load a non-flipped texture asset in repeat mode.
     *
     * @param assetPath to the texture (not null)
     * @return the texture which was loaded
     */
    public static Texture loadTexture(String assetPath) {
        assert assetPath != null;

        boolean flipY = false;
        Texture texture = loadTexture(assetPath, flipY);
        return texture;
    }

    /**
     * Load a model, check the number of geometries, and verify that all
     * geometries can be textured.
     *
     * @param assetPath to the model (not null)
     * @param numGeometries number of geometries expected (&gt;0)
     * @return a new orphan node with numGeometries new child geometries
     */
    public static Node loadTexturedModel(String assetPath, int numGeometries) {
        assert assetPath != null;

        Node node = loadModel(assetPath, numGeometries);
        for (Spatial child : node.getChildren()) {
            assert child instanceof Geometry;
            /*
             * Make sure the mesh includes texture coordinates.
             */
            Geometry geometry = (Geometry) child;
            Mesh mesh = geometry.getMesh();
            if (!Misc.hasUV(mesh)) {
                throw new AssetLoadException(
                        "UV data is missing from " + assetPath);
            }
        }

        return node;
    }

    /**
     * Load an XML asset.
     *
     * @param assetPath to the XML (not null)
     * @return a new DOM document
     */
    public static Document loadXml(String assetPath) {
        assert assetPath != null;

        Document document = null;
        try {
            document = (Document) manager.loadAsset(assetPath);
        } catch (AssetNotFoundException exception) {
            logger.log(Level.SEVERE, "XML asset {0} not found.",
                    MyString.quote(assetPath));
        }
        assert document != null;
        return document;
    }

    /**
     * Initialize this class.
     *
     * @param assetManager (not null)
     */
    public static void setManager(AssetManager assetManager) {
        assert assetManager != null;
        manager = assetManager;
        /*
         * Register a loader for XML files (such as world maps).
         */
        manager.registerLoader(XmlLoader.class, "xml");
    }

    /**
     * Enable/disable shaded materials.
     *
     * @param newValue true to enable shading, false to disable it
     */
    public static void setShadingFlag(boolean newValue) {
        shadingFlag = newValue;
    }
    // *************************************************************************
    // private methods

    /**
     * Create a material (shaded if shadingFlag is set) from a diffuse texture.
     *
     * @param diffuseTexture (not null)
     * @return a new instance
     */
    private static Material createMaterial(Texture diffuseTexture) {
        Material material;
        if (shadingFlag) {
            material = createShadedMaterial(diffuseTexture);
        } else {
            material = MyAsset.createUnshadedMaterial(manager, diffuseTexture);
        }
        return material;
    }

    /**
     * Create a shaded material with the specified diffuse color, shininess, and
     * specular color.
     *
     * @param diffuseColor (not null, unaffected)
     * @param shininess (&ge;0)
     * @param specularColor (not null, unaffected)
     * @return a new instance
     */
    private static Material createShadedMaterial(ColorRGBA diffuseColor,
            float shininess, ColorRGBA specularColor) {
        assert diffuseColor != null;
        assert specularColor != null;

        Material material = loadMaterial(shadedMaterialAssetPath);
        material.setBoolean("UseMaterialColors", true);
        material.setColor("Ambient", diffuseColor);
        material.setColor("Diffuse", diffuseColor);
        material.setColor("Specular", specularColor);
        material.setFloat("Shininess", shininess);
        return material;
    }

    /**
     * Create a matte shaded material with the specified texture.
     *
     * @param texture (not null)
     */
    private static Material createShadedMaterial(Texture texture) {
        assert texture != null;

        Material material = loadMaterial("MatDefs/matte/matte.j3md");
        material.setTexture("Tex1", texture);
        return material;
    }

    /**
     * Create a generic terrain material without tri-planar mapping.
     */
    private static Material createTerrainMaterial() {
        Material material;
        if (shadingFlag) {
            material = loadMaterial("MatDefs/matte/matte.j3md");
        } else {
            material = loadMaterial(terrainMaterialAssetPath);
            material.setBoolean("useTriPlanarMapping", false);
        }
        return material;
    }

    /**
     * Load a block texture.
     *
     * @param textureName (not null)
     */
    private static Texture loadBlockTexture(String textureName) {
        assert textureName != null;

        String assetPath = String.format("Textures/blocks/%s.jpg", textureName);
        Texture texture = loadTexture(assetPath);
        return texture;
    }

    /**
     * Load a model, substituting a BLEND asset if the J3O is not available.
     *
     * @param assetPath preferred asset path to the model (not null)
     * @return a new orphan node
     */
    private static Node loadModel(String assetPath) {
        assert assetPath != null;

        Spatial model;
        try {
            model = manager.loadModel(assetPath);
        } catch (AssetNotFoundException exception) {
            if (!assetPath.endsWith(".j3o")) {
                throw exception;
            }
            logger.log(Level.WARNING, "Model asset {0} not found.",
                    MyString.quote(assetPath));
            /*
             * Try again with ".blend" extension in place of ".j3o".
             */
            int end = assetPath.length() - ".j3o".length();
            String altPath = assetPath.substring(0, end) + ".blend";
            model = manager.loadModel(altPath);
        }

        assert model instanceof Node;
        Node result = (Node) model;

        assert MySpatial.isOrphan(result);
        return result;
    }

    /**
     * Load a texture asset in repeat mode, substituting a PNG asset if the JPEG
     * is not available.
     *
     * @param assetPath preferred asset path to the texture (not null)
     * @param flipY if true, flip the texture's Y axis, else don't flip
     * @return the texture which was loaded
     */
    private static Texture loadTexture(String assetPath, boolean flipY) {
        assert assetPath != null;

        TextureKey key = new TextureKey(assetPath, flipY);
        Texture texture;
        try {
            texture = manager.loadTexture(key);

        } catch (AssetNotFoundException exception) {
            if (!assetPath.endsWith(".jpg")) {
                throw exception;
            }
            logger.log(Level.WARNING, "Texture asset {0} not found.",
                    MyString.quote(assetPath));
            /*
             * Try again with ".png" extension in place of ".jpg".
             */
            int end = assetPath.length() - ".jpg".length();
            String altPath = assetPath.substring(0, end) + ".png";
            key = new TextureKey(altPath, flipY);
            texture = manager.loadTexture(key);
        }
        texture.setWrap(Texture.WrapMode.Repeat);
        return texture;
    }
}